package cn.tedu.mall.seckill.service.impl;

import cn.tedu.mall.common.exception.CoolSharkServiceException;
import cn.tedu.mall.common.pojo.domain.CsmallAuthenticationInfo;
import cn.tedu.mall.common.restful.ResponseCode;
import cn.tedu.mall.order.service.IOmsOrderService;
import cn.tedu.mall.pojo.order.dto.OrderAddDTO;
import cn.tedu.mall.pojo.order.dto.OrderItemAddDTO;
import cn.tedu.mall.pojo.order.vo.OrderAddVO;
import cn.tedu.mall.pojo.seckill.dto.SeckillOrderAddDTO;
import cn.tedu.mall.pojo.seckill.model.Success;
import cn.tedu.mall.pojo.seckill.vo.SeckillCommitVO;
import cn.tedu.mall.seckill.config.RabbitMqComponentConfiguration;
import cn.tedu.mall.seckill.service.ISeckillService;
import cn.tedu.mall.seckill.utils.SeckillCacheUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@Slf4j
public class SeckillServiceImpl implements ISeckillService {

    // dubbo调用生成订单的方法,需要dubbo装配
    @DubboReference
    private IOmsOrderService dubboOrderService;
    // 操作Redis中的数值类型,预热时使用的就是StringRedisTemplate
    // 现在要在Redis中操作库存数,也需要使用StringRedisTemplate
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    // RabbitMQ发送消息队列
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /*
    1.判断用户是否为重复购买和Redis中该Sku是否有库存
    2.秒杀订单转换成普通订单,需要使用dubbo调用order模块的生成订单方法
    3.使用消息队列(RabbitMQ)将秒杀成功记录信息保存到success表中
    4.秒杀订单信息返回
     */

    @Override
    public SeckillCommitVO commitSeckill(SeckillOrderAddDTO seckillOrderAddDTO) {
        // 第一步: 判断用户是否为重复购买和Redis中该Sku是否有库存
        // 从SpringSecurity上下文中获取userId
        Long userId=getUserId();
        // 从方法参数中订单项属性里获取商品skuId
        Long skuId=seckillOrderAddDTO
                .getSeckillOrderItemAddDTO().getSkuId();
        // 这样我们就确认了当前登录用户要购买哪个商品,也就是userId和skuId的确认
        // 秒杀规定每个用户只能购买每个商品(sku)一次,使用userId和skuId生成一个key
        // mall:seckill:reseckill:2:1
        String reSeckillCheckKey= SeckillCacheUtils
                            .getReseckillCheckKey(skuId,userId);
        // 确定了上面的key之后,我们利用stringRedisTemplate掉用一个increment方法
        // 这个方法的效果如下
        // 1.如果上面的key不存在,redis会创建这个key,而且值为1并保存
        // 2.如果上面的key存在,redis会在当前这个key值的基础上+1并保存
        //   举例说明,如果当前redis key值已经是1,再运行increment就会变为2
        // 这个方法还有一个非常重要的效果,就是会将increment方法运行后的值返回
        // 所以我们使用seckillTimes变量接收,接收的值就是当前用户购买这个商品的次数
        Long seckillTimes= stringRedisTemplate
                .boundValueOps(reSeckillCheckKey).increment();
        // 如果购买次数大于1
        if(seckillTimes > 100){
            // 已经购买过了,抛出异常终止程序
            throw new CoolSharkServiceException(
                    ResponseCode.FORBIDDEN,
                    "您已经购买过这个商品了,谢谢您的支持");
        }
        // 程序运行到此处,表示用户确实是第一次购买这个商品
        // 下面要判断这个商品是否还有库存
        // 库存是缓存预热时保存在Redis中的,先来确定key
        // "mall:seckill:sku:stock:1"
        String skuStockKey=SeckillCacheUtils.getStockKey(skuId);
        // 判断Redis中这个key是否存在
        if( ! stringRedisTemplate.hasKey(skuStockKey)){
            // 进入if表示没有这个key,抛出异常给提示
            throw new CoolSharkServiceException(
                    ResponseCode.INTERNAL_SERVER_ERROR,
                    "当前商品没有预热库存(可能在缓存真空期,等下一分钟再试)");
        }
        // 库存正常预热,在Redis中,我们就要对库存数进行减少操作
        // 我们调用和increment方法效果相反的decrement方法
        // 接收库存数减少后的返回值
        Long leftStock=stringRedisTemplate
                .boundValueOps(skuStockKey).decrement();
        // leftStock是decrement方法减1之后的库存数
        // leftStock是0时,表示当前用购买到了最后一个库存
        // 只有leftStock<0时,才是库存不足
        if(leftStock<-100){
            // 库存不足,抛出异常,终止购买
            // 但是上面代码中,已经记录当前用户购买次数为1,但是因为库存不足没买到
            // 所以要将当前用户购买次数恢复为0,之后才能再次购买
            stringRedisTemplate
                    .boundValueOps(reSeckillCheckKey).decrement();
            throw new CoolSharkServiceException(
                    ResponseCode.BAD_REQUEST,
                    "对不起,您要购买的商品已经售罄");
        }
        // 到此为止,当前用户就通过了重复购买和库存剩余的验证
        // 下面可以开始生成订单了
        // 第二步:秒杀订单转换成普通订单,需要使用dubbo调用order模块的生成订单方法
        // 先编写一个方法进行转换
        OrderAddDTO orderAddDTO=
                convertSeckillOrderToOrder(seckillOrderAddDTO);
        // 经过上面的方法,我们完成了秒杀订单到普通订单的转换
        // 但是userId还是没有正常的赋值到orderAddDTO中
        // 这里必须给它赋值,否则我们dubbo调用的order模块是无法获取用户信息的
        orderAddDTO.setUserId(userId);
        // dubbo调用执行新增订单的方法
        // 现阶段先了解同步调用订单即可,课程后期会扩展异步调用的流程
         OrderAddVO orderAddVO=dubboOrderService.addOrder(orderAddDTO);
        /*rabbitTemplate.convertAndSend(
                RabbitMqComponentConfiguration.SECKILL_EX,
                RabbitMqComponentConfiguration.SECKILL_RK,
                orderAddDTO);*/
        // 第三步:使用消息队列(RabbitMQ)将秒杀成功记录信息保存到success表中
        // 秒杀成功记录,需求是将哪个用户购买了哪个商品保存在数据库,用于秒杀后续的数据统计
        // 它的运行就不是非常迫切的,可以在服务器忙过后在延迟运行
        // 这时就使用消息队列,进行削峰填谷
        // 实例化Success对象
        Success success= new Success();
        // 观察success对象的属性能发现,大部分信息都是sku的
        // 所以将秒杀订单项同名属性(sku的)赋值到success即可
        BeanUtils.copyProperties(
                seckillOrderAddDTO.getSeckillOrderItemAddDTO(),
                success);
        // 再把success中额外的属性手动赋值
        success.setUserId(userId);
        // success秒杀价属性名被SeckillOrderItemAddDTO不同,需要手动赋值
        success.setSeckillPrice(seckillOrderAddDTO
                .getSeckillOrderItemAddDTO().getPrice());
        success.setOrderSn(orderAddVO.getSn());
        // 使用RabbitMQ发送success消息
        rabbitTemplate.convertAndSend(
                RabbitMqComponentConfiguration.SECKILL_EX,
                RabbitMqComponentConfiguration.SECKILL_RK,
                success);
        // 消息已发出,当前方法任务就完成了,无需考虑接收问题
        // 第四步:秒杀订单信息返回
        // 当前方法返回值类型设计为SeckillCommitVO
        // 经观察,和新增订单返回值OrderAddVO完全一致,直接同名属性赋值即可
        SeckillCommitVO commitVO=new SeckillCommitVO();
        BeanUtils.copyProperties(orderAddVO,commitVO);
        // 最后返回commitVO即可
        return commitVO;
    }

    // 将秒杀订单转换为普通订单的方法
    private OrderAddDTO convertSeckillOrderToOrder(
                    SeckillOrderAddDTO seckillOrderAddDTO) {
        // 实例化普通订单对象,用于返回
        OrderAddDTO orderAddDTO = new OrderAddDTO();
        // 秒杀订单信息中同名属性赋值到普通订单
        BeanUtils.copyProperties(seckillOrderAddDTO,orderAddDTO);
        // 上面可以为普通订单信息的大部分属性赋值(除订单项信息之外)
        // 秒杀订单信息的订单项属性是个对象,而普通订单是个集合
        // 所以我们要先将秒杀订单项转换为普通订单项
        // 也就是SeckillOrderItemAddDTO转换为OrderItemAddDTO
        // 先实例化普通订单项
        OrderItemAddDTO orderItemAddDTO=new OrderItemAddDTO();
        // 经观察,两个订单项对象属性都同名,直接赋值即可
        BeanUtils.copyProperties(
                seckillOrderAddDTO.getSeckillOrderItemAddDTO(),
                orderItemAddDTO);
        // 双方的属性值赋值完毕后,要实例化一个普通订单项的集合
        List<OrderItemAddDTO> list=new ArrayList<>();
        // 把赋好值的订单项保存到集合中
        list.add(orderItemAddDTO);
        // list对象需要赋值到订单对象中
        orderAddDTO.setOrderItems(list);
        // 最后别忘了返回转换完成的对象
        return orderAddDTO;
    }

    public CsmallAuthenticationInfo getUserInfo(){
        // 获取SpringSecurity上下文对象
        UsernamePasswordAuthenticationToken token=
                (UsernamePasswordAuthenticationToken)
                        SecurityContextHolder.getContext().getAuthentication();
        // 为了程序逻辑严谨,判断一下token是否为null
        if(token == null){
            throw new CoolSharkServiceException(
                    ResponseCode.UNAUTHORIZED,"您还没有登录");
        }
        // 从SpringSecurity上下文对象中获取用户信息
        CsmallAuthenticationInfo userInfo=
                (CsmallAuthenticationInfo) token.getCredentials();
        // 最终返回用户信息
        return userInfo;
    }
    public Long getUserId(){
        return getUserInfo().getId();
    }

}
